// 流线图
var Windy = function Windy(params) {

    var VELOCITY_SCALE = 0.01 * (Math.pow(window.devicePixelRatio, 1 / 3) || 1); // scale for wind velocity (completely arbitrary--this value looks nice)
    var MIN_TEMPERATURE_K = 261.15; // step size of particle intensity color scale
    var MAX_TEMPERATURE_K = 317.15; // wind velocity at which particle intensity is maximum (m/s)
    var INTENSITY_SCALE_STEP = 1;            // step size of particle intensity color scale
    var MAX_WIND_INTENSITY = 10;              // wind velocity at which particle intensity is maximum (m/s)
    var MAX_PARTICLE_AGE = 90; // max number of frames a particle is drawn before regeneration
    var PARTICLE_LINE_WIDTH = 1; // line width of a drawn particle
    var PARTICLE_MULTIPLIER = 1 / 200; // particle count scalar (completely arbitrary--this values looks nice) 决定粒子密度，是线状还是点状
    var PARTICLE_REDUCTION = Math.pow(window.devicePixelRatio, 1 / 3) || 1.6; // multiply particle count for mobiles by this amount
    var FRAME_RATE = 15,
        FRAME_TIME = 1000 / FRAME_RATE; // desired frames per second

    var NULL_WIND_VECTOR = [NaN, NaN, null]; // singleton for no wind in the form: [u, v, magnitude]

    var builder;
    var grid;
    var date;
    var λ0, φ0, Δλ, Δφ, ni, nj;

    // interpolation for vectors like wind (u,v,m)
    var bilinearInterpolateVector = function bilinearInterpolateVector(x, y, g00, g10, g01, g11) {
        var rx = 1 - x;
        var ry = 1 - y;
        var a = rx * ry,
            b = x * ry,
            c = rx * y,
            d = x * y;
        var u = g00[0] * a + g10[0] * b + g01[0] * c + g11[0] * d;
        var v = g00[1] * a + g10[1] * b + g01[1] * c + g11[1] * d;
        /*var tmp = g00[2] * a + g10[2] * b + g01[2] * c + g11[2] * d;*/
        var tmp = Math.sqrt(u * u + v * v);
        return [u, v, tmp];
    };

    var createWindBuilder = function createWindBuilder(uComp, vComp) {
        var uData = uComp.data,
            vData = vComp.data;
        return {
            header: uComp.header,
            //recipe: recipeFor("wind-" + uComp.header.surface1Value),
            data: function data(i) {
                return [uData[i], vData[i],  Math.sqrt(uData[i] * uData[i] + vData[i] * vData[i])];
            },
            interpolate: bilinearInterpolateVector
        };
    };

    var createBuilder = function createBuilder(data) {
        var uComp = null,
            vComp = null,
            scalar = null;

        data.forEach(function (record) {
            switch (record.header.parameterCategory) {
                case 0://current
                    uComp = record;break;
                case 1:
                    vComp = record;break;
            }

            // switch (record.header.parameterCategory + "," + record.header.parameterNumber) {
            // 	case "0,192"://current
            // 	case "0,194"://swell wave height
            // 	case "0,196"://swell wave period
            // 	case "0,10": // new swell wave
            // 	case "2,2": // wind
            // 	case "0,6": // wind wave
            // 		uComp = record;break;
            // 	case "0,193":
            // 	case "0,195":
            // 	case "0,197":
            // 	case "0,11":
            // 	case "2,3":
            // 	case "0,4":
            // 		vComp = record;break;
            // 	case "0,0":
            // 		temp = record;break;
            // 	default:
            // 		scalar = record;
            // }
        });

        return createWindBuilder(uComp, vComp);
    };

    var buildGrid = function buildGrid(data, callback) {

        builder = createBuilder(data);
        var header = builder.header;

        λ0 = header.lo1;
        φ0 = header.la1; // the grid's origin (e.g., 0.0E, 90.0N)

        Δλ = header.dx;
        Δφ = header.dy; // distance between grid points (e.g., 2.5 deg lon, 2.5 deg lat)

        ni = header.nx;
        nj = header.ny; // number of grid points W-E and N-S (e.g., 144 x 73)

        date = new Date(header.refTime);
        date.setHours(date.getHours() + header.forecastTime);

        // Scan mode 0 assumed. Longitude increases from λ0, and latitude decreases from φ0.
        // http://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_table3-4.shtml
        grid = [];
        var p = 0;
        var isContinuous = Math.floor(ni * Δλ) >= 360;

        for (var j = 0; j < nj; j++) {
            var row = [];
            for (var i = 0; i < ni; i++, p++) {
                row[i] = builder.data(p);
            }
            if (isContinuous) {
                // For wrapped grids, duplicate first column as last column to simplify interpolation logic
                row.push(row[0]);
            }
            grid[j] = row;
        }

        callback({
            date: date,
            interpolate: interpolate
        });
    };

    /**
     * Get interpolated grid value from Lon/Lat position
     * @param λ {Float} Longitude
     * @param φ {Float} Latitude
     * @returns {Object}
     */
    var interpolate = function interpolate(λ, φ) {

        if (!grid) return null;

        var i = floorMod(λ - λ0, 360) / Δλ; // calculate longitude index in wrapped range [0, 360)
        var j = (φ0 - φ) / Δφ; // calculate latitude index in direction +90 to -90

        var fi = Math.floor(i),
            ci = fi + 1;
        var fj = Math.floor(j),
            cj = fj + 1;

        var row;
        if (row = grid[fj]) {
            var g00 = row[fi];
            var g10 = row[ci];
            if (isValue(g00) && isValue(g10) && (row = grid[cj])) {
                var g01 = row[fi];
                var g11 = row[ci];
                if (isValue(g01) && isValue(g11)) {
                    // All four points found, so interpolate the value.
                    return builder.interpolate(i - fi, j - fj, g00, g10, g01, g11);
                }
            }
        }
        return null;
    };

    /**
     * @returns {Boolean} true if the specified value is not null and not undefined.
     */
    var isValue = function isValue(x) {
        return !(!x || !x[0] || !x[1] || x[0] == 9999.0 || x[1] == 9999.0);
    };

    /**
     * @returns {Number} returns remainder of floored division, i.e., floor(a / n). Useful for consistent modulo
     *          of negative numbers. See http://en.wikipedia.org/wiki/Modulo_operation.
     */
    var floorMod = function floorMod(a, n) {
        return a - n * Math.floor(a / n);
    };

    /**
     * @returns {Number} the value x clamped to the range [low, high].
     */
    var clamp = function clamp(x, range) {
        return Math.max(range[0], Math.min(x, range[1]));
    };

    /**
     * @returns {Boolean} true if agent is probably a mobile device. Don't really care if this is accurate.
     */
    var isMobile = function isMobile() {
        return (/android|blackberry|iemobile|ipad|iphone|ipod|opera mini|webos/i.test(navigator.userAgent)
        );
    };

    /**
     * Calculate distortion of the wind vector caused by the shape of the projection at point (x, y). The wind
     * vector is modified in place and returned by this function.
     */
    var distort = function distort(projection, λ, φ, x, y, scale, wind, windy) {
        var u = wind[0] * scale;
        var v = wind[1] * scale;
        var d = distortion(projection, λ, φ, x, y, windy);

        // Scale distortion vectors by u and v, then add.
        wind[0] = d[0] * u + d[2] * v;
        wind[1] = d[1] * u + d[3] * v;
        return wind;
    };

    var distortion = function distortion(projection, λ, φ, x, y, windy) {
        var τ = 2 * Math.PI;
        var H = Math.pow(10, -5.2);
        var hλ = λ < 0 ? H : -H;
        var hφ = φ < 0 ? H : -H;

        var pλ = project(φ, λ + hλ, windy);
        var pφ = project(φ + hφ, λ, windy);

        // Meridian scale factor (see Snyder, equation 4-3), where R = 1. This handles issue where length of 1º λ
        // changes depending on φ. Without this, there is a pinching effect at the poles.
        var k = Math.cos(φ / 360 * τ);
        return [(pλ[0] - x) / hλ / k, (pλ[1] - y) / hλ / k, (pφ[0] - x) / hφ, (pφ[1] - y) / hφ];
    };

    var createField = function(rows, bounds, callback) {
        var xMin = bounds.x;
        var field = {};

        field.release = function () {
            rows = [];
        };

        /**
         * Copies the array [x, y, u, v, m] into Array 'a' starting at index i. If x or y is out of bounds,
         * then sets the u value to NaN.
         *
         * @param x {Number}
         * @param y {Number}
         * @param a {Float32Array}
         * @param i {Number}
         */
        field.move = function(x, y, a, i) {
            var k = Math.round(y);
            if (0 <= k && k < rows.length) {
                var row = rows[k];
                var j = (Math.round(x) - xMin) * 3;
                if (row && 0 <= j && j < row.length) {
                    a[i  ] = x;
                    a[i+1] = y;
                    a[i+2] = row[j  ];
                    a[i+3] = row[j+1];
                    a[i+4] = row[j+2];
                    return;
                }
            }
            a[i  ] = x;
            a[i+1] = y;
            a[i+2] = NaN;
            a[i+3] = NaN;
            a[i+4] = NaN;
        };

        /**
         * @returns {boolean} true if the field is valid at the point (x, y)
         */
        field.isDefined = function(x, y) {
            var k = Math.round(y);
            if (0 <= k && k < rows.length) {
                var row = rows[k];
                var j = (Math.round(x) - xMin) * 3;
                if (row && 0 <= j && j < row.length) {
                    return row[j] === row[j];
                }
            }
            return false;
        };

        /**
         * @returns {boolean} true if the point (x, y) lies inside the outer boundary of the vector field, even if
         *          the vector field has a hole (is undefined) at that point, such as at an island in a field of
         *          ocean currents.
         */
        field.isInsideBoundary = function(x, y) {
            var a = new Float32Array(5);  // [x, y, u, v, m]
            field.move(x, y, a, 0);
            return a[4] === a[4];  // true if magnitude is defined or is HOLE_VECTOR
        };

        callback(bounds, field);
    };

    var buildBounds = function buildBounds(bounds, width, height) {
        var upperLeft = bounds[0];
        var lowerRight = bounds[1];
        var x = Math.round(upperLeft[0]); //Math.max(Math.floor(upperLeft[0], 0), 0);
        var y = Math.max(Math.floor(upperLeft[1], 0), 0);
        var xMax = Math.min(Math.ceil(lowerRight[0], width), width - 1);
        var yMax = Math.min(Math.ceil(lowerRight[1], height), height - 1);
        return { x: x, y: y, xMax: width, yMax: yMax, width: width, height: height };
    };

    var deg2rad = function deg2rad(deg) {
        return deg / 180 * Math.PI;
    };

    var rad2deg = function rad2deg(ang) {
        return ang / (Math.PI / 180.0);
    };

    var invert = function invert(x, y, windy) {
        var mapLonDelta = windy.east - windy.west;
        var worldMapRadius = windy.width / rad2deg(mapLonDelta) * 360 / (2 * Math.PI);
        var mapOffsetY = worldMapRadius / 2 * Math.log((1 + Math.sin(windy.south)) / (1 - Math.sin(windy.south)));
        var equatorY = windy.height + mapOffsetY;
        var a = (equatorY - y) / worldMapRadius;

        var lat = 180 / Math.PI * (2 * Math.atan(Math.exp(a)) - Math.PI / 2);
        var lon = rad2deg(windy.west) + x / windy.width * rad2deg(mapLonDelta);
        return [lon, lat];
    };

    var mercY = function mercY(lat) {
        return Math.log(Math.tan(lat / 2 + Math.PI / 4));
    };

    var project = function project(lat, lon, windy) {
        // both in radians, use deg2rad if neccessary
        var ymin = mercY(windy.south);
        var ymax = mercY(windy.north);
        var xFactor = windy.width / (windy.east - windy.west);
        var yFactor = windy.height / (ymax - ymin);

        var y = mercY(deg2rad(lat));
        var x = (deg2rad(lon) - windy.west) * xFactor;
        var y = (ymax - y) * yFactor; // y points south
        return [x, y];
    };

    function random(min, max) {
        if (max == null) {
            max = min;
            min = 0;
        }
        return min + Math.floor(Math.random() * (max - min + 1));
    };

    var interpolateField = function(bounds, extent, callback) {

        var projection = {};

        var mapArea = (extent.south - extent.north) * (extent.west - extent.east);
        var velocityScale = params.particles.velocityScale / Math.pow(2, (params.zoom - params.minZoom));

        var rows = [];
        var y = bounds.y;

        function interpolateRow(y) {
            var row = new Float32Array(bounds.width * 3); // [u0, v0, m0, u1, v1, m1, ...]
            for (var x = bounds.x, i = 0; x <= bounds.xMax; x += 2, i += 6) {
                var coord = invert(x, y, extent);
                if (coord) {
                    var λ = coord[0], φ = coord[1];
                    if (λ === λ) {
                        var wind = interpolate(λ, φ);
                        if (wind) {
                            wind = distort(projection, λ, φ, x, y, velocityScale, wind, extent);
                            row[i  ] = wind[0];
                            row[i+1] = wind[1];
                            row[i+2] = wind[2];
                            row[i+3] = wind[0];
                            row[i+4] = wind[1];
                            row[i+5] = wind[2];
                        }
                    }
                }
            }
            rows[y+1] = rows[y] = row;
        }
        var startTime = new Date().getTime();
        for (; y < bounds.yMax; y += 2) {
            interpolateRow(y);
        }
        var endTime = new Date().getTime();
        var second = endTime - startTime
        console.log('flow layer resolve data take up time：' + second + 'ms');

        createField(rows, bounds, callback);
    };

    function asColorStyle(r, g, b, a) {
        return "rgba(" + 243 + ", " + 243 + ", " + 238 + ", " + a + ")";
    }

    function hexToR(h) {
        return parseInt(cutHex(h).substring(0, 2), 16);
    }
    function hexToG(h) {
        return parseInt(cutHex(h).substring(2, 4), 16);
    }
    function hexToB(h) {
        return parseInt(cutHex(h).substring(4, 6), 16);
    }
    function cutHex(h) {
        return h.charAt(0) == "#" ? h.substring(1, 7) : h;
    }

    function windIntensityColorScale(step, maxWind) {
        var opacity = 1;
        var result = [
            /*"rgba(" + hexToR('#00ffff') + ", " + hexToG('#00ffff') + ", " + hexToB('#00ffff') + ", " + opacity + ")",
            "rgba(" + hexToR('#64f0ff') + ", " + hexToG('#64f0ff') + ", " + hexToB('#64f0ff') + ", " + opacity + ")",
            "rgba(" + hexToR('#87e1ff') + ", " + hexToG('#87e1ff') + ", " + hexToB('#87e1ff') + ", " + opacity + ")",
            "rgba(" + hexToR('#a0d0ff') + ", " + hexToG('#a0d0ff') + ", " + hexToB('#a0d0ff') + ", " + opacity + ")",
            "rgba(" + hexToR('#b5c0ff') + ", " + hexToG('#b5c0ff') + ", " + hexToB('#b5c0ff') + ", " + opacity + ")",
            "rgba(" + hexToR('#c6adff') + ", " + hexToG('#c6adff') + ", " + hexToB('#c6adff') + ", " + opacity + ")",
            "rgba(" + hexToR('#d49bff') + ", " + hexToG('#d49bff') + ", " + hexToB('#d49bff') + ", " + opacity + ")",
            "rgba(" + hexToR('#e185ff') + ", " + hexToG('#e185ff') + ", " + hexToB('#e185ff') + ", " + opacity + ")",
            "rgba(" + hexToR('#ec6dff') + ", " + hexToG('#ec6dff') + ", " + hexToB('#ec6dff') + ", " + opacity + ")",
            "rgba(" + hexToR('#ff1edb') + ", " + hexToG('#ff1edb') + ", " + hexToB('#ff1edb') + ", " + opacity + ")"*/

            "rgba(" + hexToR('#FFDC73') + ", " + hexToG('#FFDC73') + ", " + hexToB('#FFDC73') + ", " + opacity + ")",
            "rgba(" + hexToR('#FF746B') + ", " + hexToG('#FF746B') + ", " + hexToB('#FF746B') + ", " + opacity + ")",
            "rgba(" + hexToR('#E77769') + ", " + hexToG('#E77769') + ", " + hexToB('#E77769') + ", " + opacity + ")",
            "rgba(" + hexToR('#ff0000') + ", " + hexToG('#ff0000') + ", " + hexToB('#ff0000') + ", " + opacity + ")",
            "rgba(" + hexToR('#ec6dff') + ", " + hexToG('#ec6dff') + ", " + hexToB('#ec6dff') + ", " + opacity + ")",
            "rgba(" + hexToR('#ff1edb') + ", " + hexToG('#ff1edb') + ", " + hexToB('#ff1edb') + ", " + opacity + ")"
        ]
        result.indexFor = function(m) {  // map wind speed to a style
            return Math.floor(Math.min(m, maxWind) / maxWind * (result.length - 1));
        };
        return result;
    }

    var particles, animationLoop;
    var animate = function(bounds, field, extent) {

        var colorStyles = windIntensityColorScale(INTENSITY_SCALE_STEP, params.particles.maxIntensity);
        var buckets = colorStyles.map(function () {
            return [];
        });
        var mapArea = (extent.south - extent.north) * (extent.west - extent.east);

        var particleCount = Math.round(bounds.width * bounds.height * params.particles.multiplier * Math.pow(mapArea, 0.24));
        if (isMobile()) {
            particleCount = Math.floor(particleCount * PARTICLE_REDUCTION);
        }

        var particles = new Float32Array(particleCount * 5);
        var ages = new Int32Array(particleCount);
        var batches = colorStyles.map(function() { return new Float32Array(particleCount * 4); });
        var sizes = new Int32Array(batches.length);
        var xMin = bounds.x, yMin = bounds.y, width = bounds.width, height = bounds.height;

        function randomize(i) {
            var x = xMin + Math.random() * width;
            var y = yMin + Math.random() * height;
            field.move(x, y, particles, i);
        }

        function randomizeWell(i) {  // This function is hrm, but avoids "pulsing"
            for (var attempts = 0; attempts < 10; attempts++) {
                randomize(i);
                if (particles[i+2] === particles[i+2]) return;
            }
        }

        var g = params.canvas.getContext("2d");
        g.lineWidth = params.particles.lineWidth;

        var maxAge, evolve;
        if (params.particles.waves) {
            maxAge = 200;
            evolve = evolveWaves;
            g.fillStyle = "rgba(0, 0, 0, 0.90)";
        } else {
            maxAge = 100;
            evolve = evolveParticles;
            g.fillStyle = "rgba(0, 0, 0, 0.97)";//µ.isFF() ? "rgba(0, 0, 0, 0.95)" : "rgba(0, 0, 0, 0.97)";  // FF Mac alpha behaves oddly
        }

        for (var i = 0, j = 0; i < particleCount; i += 1, j += 5) {
            if (params.particles.waves) {
                ages[i] = random(0, maxAge);
            } else {
                ages[i] = random(0, maxAge);
            }
            randomizeWell(j);
        }

        var easeFactor = new Float32Array(maxAge);
        for (var k = 0; k < easeFactor.length; k++) {
            easeFactor[k] = (Math.sin(-Math.PI/2 + k/7)/2 + 1/2);  // fade in/out line intensity
        }

        function evolveWaves() {
            for (var s = 0; s < sizes.length; s++) {
                sizes[s] = 0;
            }
            for (var i = 0, j = 0; i < particleCount; i += 1, j += 5) {
                if (++ages[i] >= maxAge) {
                    ages[i] = 0;
                    randomize(j);
                }

                var x = particles[j];
                var y = particles[j + 1];
                var u = particles[j + 2];
                var v = particles[j + 3];
                var xt = x + u * 1.5;
                var yt = y + v * 1.5;
                var m = particles[j + 4];

                if (m !== m || !field.isDefined(xt, yt)) {
                    ages[i] = maxAge;  // particle has escaped the game grid
                } else {
                    particles[j] = xt;
                    particles[j + 1] = yt;

                    // width of wave
                    var mag = Math.sqrt(u*u + v*v) / 2.5;  // CONSIDER: would be nice to retain unscaled m...
                    var du = u / mag, dv = v / mag;

                    // Path from (x,y) to (xt,yt) is visible, so add this particle to the appropriate draw bucket.
                    var si = colorStyles.indexFor(m * easeFactor[ages[i]]);
                    var sj = 4 * sizes[si]++;
                    var batch = batches[si];
                    batch[sj  ] = x - dv;
                    batch[sj+1] = y + du;
                    batch[sj+2] = x + dv;
                    batch[sj+3] = y - du;
                }
            }
        }

        function evolveParticles() {
            for (var s = 0; s < sizes.length; s++) {
                sizes[s] = 0;
            }
            for (var i = 0, j = 0; i < particleCount; i += 1, j += 5) {
                if (++ages[i] >= maxAge) {
                    ages[i] = 0;
                    randomize(j);
                }

                var x = particles[j];         // x
                var y = particles[j+1];       // y
                var xt = x + particles[j+2];  // u
                var yt = y + particles[j+3];  // v
                var m = particles[j+4];       // m

                if (xt === xt) {
                    field.move(xt, yt, particles, j);
                    var u = particles[j + 2];
                    if (u === u) {
                        // Path from (x,y) to (xt,yt) is visible, so add this particle to the appropriate draw bucket.
                        var si = colorStyles.indexFor(m);
                        var sj = 4 * sizes[si]++;
                        var batch = batches[si];
                        batch[sj  ] = x;
                        batch[sj+1] = y;
                        batch[sj+2] = xt;
                        batch[sj+3] = yt;
                    } else {
                        ages[i] = maxAge;  // particle has escaped the game grid
                    }
                } else {
                    ages[i] = maxAge;  // particle has escaped the game grid
                }
            }
        }

        function draw() {
            // Fade existing trails.
//	        g.globalCompositeOperation = "destination-in";
//	        g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
//	        g.globalCompositeOperation = "source-over";

            g.save();
            g.globalAlpha = .16;
            g.globalCompositeOperation = 'destination-out';
            g.fillStyle = '#000';
            g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
            g.restore();

            // Draw new trails.
            for (var i = 0; i < batches.length; i++) {
                var batch = batches[i];
                var size = 4 * sizes[i];
                if (size > 0) {
                    g.beginPath();

                    if(params.curOverlay == 'wind') {
                        g.strokeStyle = colorStyles[i];
                    } else {
                        g.strokeStyle = "rgba(225, 225, 225, 1)"
                    }

                    for (var j = 0; j < size; j += 4) {
                        g.moveTo(batch[j  ], batch[j+1]);
                        g.lineTo(batch[j+2], batch[j+3]);
                    }
                    g.stroke();
                }
            }
            //var value = g.canvas.toDataURL("image/png")
        }

        var then = Date.now();
        (function frame() {
            animationLoop = requestAnimationFrame(frame);
            var now = Date.now();
            var delta = now - then;
            if (delta > FRAME_TIME) {
                then = now - delta % FRAME_TIME;
                evolve();
                draw();
            }
        })();
    };

    /**
     velocityScale: 速度[线长]
     multiplier： 密集程度
     */
    var updateOverlayConfig = function(overlay) {
        if(!overlay) return;
        params.particles = {};
        var particlesConfigs = {
            'wind': {velocityScale: 0.06, maxIntensity: 10, lineWidth: 1, multiplier: 1/500},
            'oceanCurrent': {velocityScale: 0.5, maxIntensity: 0.7, lineWidth: 1, multiplier: 1/50},
            'wave': {velocityScale: 0.003, maxIntensity: 12, waves: true, lineWidth: 1.5, multiplier: 1/200},
            'windWave': {velocityScale: 0.005, maxIntensity: 12, waves: true, lineWidth: 1.5, multiplier: 1/200}
        };
        var particlesConfig = (overlay == 'swellFirst' || overlay == 'swellSecond') ? particlesConfigs['wave'] : particlesConfigs[overlay];
        params.curOverlay = overlay;
        for(var k in particlesConfig) {
            params.particles[k] = particlesConfig[k];
        }
    };

    var start = function start(bounds, extent, overlay, data, zoom, minZoom) {
        updateOverlayConfig(overlay);

        params.zoom = zoom;
        params.minZoom = minZoom;

        if(params.canvas) $(params.canvas).show();
        var mapBounds = {
            west: deg2rad(extent[0]),
            south: deg2rad(extent[1]),
            east: deg2rad(extent[2]),
            north: deg2rad(extent[3]),
            width: bounds[1][0],
            height: bounds[1][1]
        };
        stop(true);
        // build grid
        buildGrid(data, function (grid) {
            // interpolateField
            interpolateField(buildBounds(bounds, bounds[1][0], bounds[1][1]), mapBounds, function (bounds, field) {
                // animate the canvas with random points
                windy.field = field;
                animate(bounds, field, mapBounds);
            });
        });
    };

    var stop = function stop(flag) {
        if(!flag && params.canvas) $(params.canvas).hide();
        if (windy.field) windy.field.release();
        if (animationLoop) cancelAnimationFrame(animationLoop);
    };

    var shift = function shift(dx, dy) {
        var canvas = params.canvas,
            w = canvas.width,
            h = canvas.height,
            ctx = canvas.getContext("2d");
        if (w > dx && h > dy) {
            var clamp = function clamp(high, value) {
                return Math.max(0, Math.min(high, value));
            };
            var imageData = ctx.getImageData(clamp(w, -dx), clamp(h, -dy), clamp(w, w - dx), clamp(h, h - dy));
            ctx.clearRect(0, 0, w, h);
            ctx.putImageData(imageData, clamp(w, dx), clamp(h, dy));
            for (var i = 0, pLength = particles.length; i < pLength; i++) {
                particles[i].x += dx;
                particles[i].y += dy;
            }
        }
    };

    var windy = {
        params: params,
        start: start,
        stop: stop,
        shift: shift,
        createField: createField,
        interpolatePoint: interpolate
    };

    // shim layer with setTimeout fallback
    window.requestAnimationFrame = function () {
        return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {
            return window.setTimeout(callback, 1000 / FRAME_RATE);
        };
    }();

    if (!window.cancelAnimationFrame) {
        window.cancelAnimationFrame = function (id) {
            clearTimeout(id);
        };
    }

    return windy;
};

// 等值线
function Isoline(options) {

    var _canvas = options.canvas;
    var _map = options.map;
    var _lineColor = options.lineColor || '#00edf9';
    var _lineWidth = options.lineWidth || 1;
    var _markBgColor = options.markBgColor || 'rgba(0, 0, 255, 0.4)';
    var _markTextColor = options.markTextColor || '#FFFFFF';
    var _levelFilter = null;

    /**
     * @Description: 绘制等值线
     * @Author: songwj
     * @Date: 2018年5月23日 下午5:30:44
     * @param isolineCtx 画笔对象
     * @param productType 产品类型
     * @param zoom 地图缩放等级
     * @param level 线条等级
     * @param linePoints 线条像素坐标点数据
     * @param leftDelta 左边偏移量
     * @param rightDelta 右边偏移量
     * @param zeroDegreeXPxl
     * @param zeroDegreeYPxl
     * @param twentyDegreeYPxl
     * @param earthW
     */
    function drawContour(isolineCtx, productType, zoom, level, linePoints, unit, canvasW, canvasH, lonPixelOffset) {
        var pntsNum = linePoints.length;// 线条点数
        /*var pointNumFlag = ((pntsNum>2000) || (pntsNum>2000 && zoom>1) || (pntsNum>1000 && zoom>4) || (pntsNum>500 && zoom>5) || (pntsNum>100 && zoom>6) || (pntsNum>50 && zoom>7));
        if (! pointNumFlag) {
          return;
        }*/

        isolineCtx.strokeStyle = _lineColor;// 线条颜色
        isolineCtx.lineWidth = _lineWidth;// 线宽
        var points = [];// 存放矫正后的所有点
        var allShowPnts = [];// 存放所有可视点

        for (var k = 0; k < pntsNum; k++) {
            var linePoint = linePoints[k];
            var pntPxl_x = linePoint.x + lonPixelOffset;
            var pntPxl_y = linePoint.y;

            if (0 <= pntPxl_x && pntPxl_x <= canvasW && 0 <= pntPxl_y && pntPxl_y <= canvasH) {
                allShowPnts.push({x: pntPxl_x, y: pntPxl_y});
            }

            points.push({x: pntPxl_x, y: pntPxl_y});
        }

        isolineCtx.beginPath();
        for (var i = 0; i < pntsNum; i++) {
            if (i == 0) {
                isolineCtx.moveTo(points[0].x, points[0].y);// 重置绘制的起始点
            } else {
                isolineCtx.lineTo(points[i].x, points[i].y);
            }
        }
        isolineCtx.stroke();

        // 绘制标注
        var linePoint = null;
        // 取中间点坐标
        var pntsLen = allShowPnts.length;
        if (pntsLen > 0) {
            var idx = pntsLen == 1 ? 1 : Math.floor(pntsLen / 2);
            linePoint = allShowPnts[idx - 1];
        } else {
            var idx = Math.floor(pntsNum / 2);
            var linePoint = points[idx - 1];
        }
        var pntPxl_x = linePoint.x;
        var pntPxl_y = linePoint.y;
        // 绘制标注外部样式
        isolineCtx.font = "10px Arial";
        level = getLevelTxt(level, productType, unit);
        level = Math.round(level);
        var halfTxtWidth = isolineCtx.measureText(level).width >> 1;// 文本宽度的一半
        // 绘制标注背景
        isolineCtx.strokeStyle = _markBgColor;
        isolineCtx.lineWidth = 15;
        isolineCtx.lineJoin = "round";// 圆角
        isolineCtx.beginPath();
        isolineCtx.moveTo(pntPxl_x - halfTxtWidth, pntPxl_y);
        isolineCtx.lineTo(pntPxl_x + halfTxtWidth, pntPxl_y);
        isolineCtx.closePath();
        isolineCtx.stroke();
        // 绘制标注
        isolineCtx.fillStyle = _markTextColor;
        isolineCtx.textBaseline = "middle";
        isolineCtx.textAlign = "center";
        isolineCtx.fillText(level, pntPxl_x, pntPxl_y);
    }

    function drawSeaWaveHeightFlag(productType, level, isolineType) {
        if (isolineType == 'isoline' || isolineType == 'heightIsoline') {
            switch (productType) {
                case "windWave":
                case "swellFirst":
                case "swellSecond":
                case "swh":
                case "oceanCurrent":
                    return level >= 2;
                default:
                    return true;
            }
        }
        return true;
    }

    // 各要素单位换算
    function getLevelTxt(level, productType, unit) {
        switch (productType) {
            case "msl":// 海平面气压//目标单位，原始单位默认认为是hpa
                return pressure_unify_fixed(level/100, "hPa",0)[0];
            case "oceanCurrent":// 海表洋流默认为m/s
                return speed_unify_fixed(level,"m/s",0)[0];
            case "sst":// 海表温度
            case "d2":// 2米露点温度
            case "t2":// 2米气温//默认为K开氏度,转换为摄氏度进行处理
                return temperature_unify(level - 273.16,"℃",0)[0];
            case "vis":/*默认单位是m米*/
                return distance_unify(level/1000,"米",0)[0];
            case "rain":
                return level.toFixed(0);
                if (level == 0.1)
                    return "小雨";
                else if (level == 5.0)
                    return "中雨";
                else if (level == 15.0)
                    return "大雨";
                else if (level == 30.0)
                    return "暴雨";
                else if (level == 70.0)
                    return "大暴雨";
                else
                    return "特大暴雨";
            default:
                return (Math.floor(level) == level ? level : level.toFixed(1));
        }
    }

    /**
     * 绘制高低压标志
     * @param isolineCtx
     * @param zoom
     * @param highData
     * @param lowData
     * @param leftOffset
     * @param rightOffset
     * @param zeroDegreeXPxl
     * @param zeroDegreeYPxl
     * @param twentyDegreeYPxl
     * @param earthW
     */
    function drawHighLowMark(isolineCtx, highData, lowData, lonPixelOffset) {
        isolineCtx.fillStyle = _lineColor;
        isolineCtx.textBaseline = "middle";
        isolineCtx.textAlign = "center";
        isolineCtx.font = "10px Arial";
        var regex = /^[-+]?\d+(\.\d+([eE][-+]\d+)?)?$/;// 数据类型校验正则

        // 绘制高压
        for (var i = 0, len = highData.length; i < len; i += 3) {
            var x = highData[i] + lonPixelOffset;
            var y = highData[i + 1];
            var val = highData[i + 2];
            isolineCtx.fillText("H", x, y - 5);
            if (regex.test(val)) {
                isolineCtx.fillText(val.toFixed(0), x, y + 7);
            }
        }

        // 绘制低压
        for (var i = 0, len = highData.length; i < len; i += 3) {
            var x = lowData[i] + lonPixelOffset;
            var y = lowData[i + 1];
            var val = lowData[i + 2];
            isolineCtx.fillText("L", x, y - 5);
            if (regex.test(val)) {
                isolineCtx.fillText(val.toFixed(0), x, y + 7);
            }
        }
    }

    /**
     * @Description: 计算获取拟合曲线上的点坐标
     *         三次B样条曲线方程：P(t)=P0 * F0,3(t) + P1 * F1,3(t) + P2 * F2,3(t) + P3 * F3,3(t)
     * @Author: songwj
     * @Date: 2018年6月27日 下午1:11:54
     * @param t 拟合曲线变量
     * @param pnt0 特征点0
     * @param pnt1 特征点1
     * @param pnt2 特征点2
     * @param pnt3 特征点3
     */
    function getLinePoints(t, pnt0, pnt1, pnt2, pnt3) {
        var a1 = Math.pow((1 - t), 3) / 6;
        var a2 = (3 * Math.pow(t, 3) - 6 * Math.pow(t, 2) + 4) / 6;
        var a3 = (-3 * Math.pow(t, 3) + 3 * Math.pow(t, 2) + 3 * t + 1) / 6;
        var a4 = Math.pow(t, 3) / 6;

        var pnt0x = pnt0.x;
        var pnt1x = pnt1.x;
        var pnt2x = pnt2.x;
        var pnt3x = pnt3.x;

        // 计算拟合曲线上对应的x,y,lon值
        var x = a1 * pnt0x + a2 * pnt1x + a3 * pnt2x + a4 * pnt3x;
        var y = a1 * pnt0.y + a2 * pnt1.y + a3 * pnt2.y + a4 * pnt3.y;
        return {x: x, y: y};
    }

    function dealIsolineData(data, isPressure, extent, zoom) {
        var productType = data.productType;// 产品类型
        var isolineType = data.isolineType;
        if (isPressure)
            data.srcData = [{
                high: [],
                low: [],
                isoline: data.srcData
            }];

        var srcData = isPressure ? data.srcData[0]["isoline"] : data.srcData;
        var returnData = [];// 存放最终返回数据
        var minLon = extent[0], minLat = extent[1], maxLon = extent[2], maxLat = extent[3];
        var displayAreaPixel = 60;

        for (var i = 0, srcLen = srcData.length; i < srcLen; i++) {
            var level = srcData[i].level;// 等级
            if (! drawSeaWaveHeightFlag(productType, level, isolineType)) {
                continue;
            }
            if (_levelFilter && !_levelFilter(level)) {
                // 不满足levelFilter，则过滤掉
                continue;
            }
            var levelDatas = {};// 存放同级别数据
            var lines = srcData[i].lines;// 等级对应的所有线条
            var linesPoints = [];// 同级别所有线条的像素点坐标数据

            for (var j = 0; j < lines.length; j++) {
                var line = lines[j];// 线条数据
                var linePoints = [];// 单线条的像素点坐标数据
                var len = line.length;
                var pntNum = len / 2;// 总点数

                // 原始数据只处理8个以上点
                if (pntNum < 8) {
                    continue;
                }

                var minX = undefined, minY = undefined, maxX = undefined, maxY = undefined;
                var lineXYPnts = [];// 存放转换后的像素点x,y点坐标
                for (var k = 0; k < len; k += 2) {
                    var lon = line[k];// 经度
                    var lat = line[k + 1];// 纬度
                    var pntPxl = OlCommon.getPixelFromLngLat([lon, lat]);
                    lineXYPnts.push({x: pntPxl.x, y: pntPxl.y});

                    if (typeof minX == 'undefined') {
                        minX = pntPxl.x;
                        maxX = pntPxl.x;
                    } else {
                        if (pntPxl.x < minX) {
                            minX = pntPxl.x;
                        } else if (pntPxl.x > maxX) {
                            maxX = pntPxl.x;
                        }
                    }
                    if (typeof minY == 'undefined') {
                        minY = pntPxl.y;
                        maxY = pntPxl.y;
                    } else {
                        if (pntPxl.y < minY) {
                            minY = pntPxl.y;
                        } else if (pntPxl.y > maxY) {
                            maxY = pntPxl.y;
                        }
                    }
                }
                if ((maxX - minX < displayAreaPixel) && (maxY - minY < displayAreaPixel)) {
                    continue;
                }

                // 闭合曲线
                if (line[0] == line[len - 2] && line[1] == line[len - 1]) {
                    for (var n = 0; n < pntNum; n++) {
                        if (n <= pntNum - 4) {
                            for (var t = 0.0; t <= 1.0; t += 0.1) {
                                var pnt0 = lineXYPnts[n];
                                var pnt1 = lineXYPnts[n + 1];
                                var pnt2 = lineXYPnts[n + 2];
                                var pnt3 = lineXYPnts[n + 3];
                                linePoints.push(getLinePoints(t, pnt0, pnt1, pnt2, pnt3));
                            }
                        } else if (n == pntNum - 3) {
                            for (var t = 0.0; t <= 1.0; t += 0.1) {
                                var pnt0 = lineXYPnts[n];
                                var pnt1 = lineXYPnts[n + 1];
                                var pnt2 = lineXYPnts[n + 2];
                                var pnt3 = lineXYPnts[0];
                                linePoints.push(getLinePoints(t, pnt0, pnt1, pnt2, pnt3));
                            }
                        } else if (n == pntNum - 2) {
                            for (var t = 0.0; t <= 1.0; t += 0.1) {
                                var pnt0 = lineXYPnts[n];
                                var pnt1 = lineXYPnts[n + 1];
                                var pnt2 = lineXYPnts[0];
                                var pnt3 = lineXYPnts[1];
                                linePoints.push(getLinePoints(t, pnt0, pnt1, pnt2, pnt3));
                            }
                        } else if (n == pntNum - 1) {
                            for (var t = 0.0; t <= 1.0; t += 0.1) {
                                var pnt0 = lineXYPnts[n];
                                var pnt1 = lineXYPnts[0];
                                var pnt2 = lineXYPnts[1];
                                var pnt3 = lineXYPnts[2];
                                linePoints.push(getLinePoints(t, pnt0, pnt1, pnt2, pnt3));
                            }
                        }
                    }
                    // 不闭合曲线
                } else {
                    linePoints.push({x: lineXYPnts[0].x, y: lineXYPnts[0].y});
                    for (var n = 0; n < pntNum; n++) {
                        if (n <= pntNum - 4) {
                            for (var t = 0.0; t <= 1.0; t += 0.1) {
                                var pnt0 = lineXYPnts[n];
                                var pnt1 = lineXYPnts[n + 1];
                                var pnt2 = lineXYPnts[n + 2];
                                var pnt3 = lineXYPnts[n + 3];
                                linePoints.push(getLinePoints(t, pnt0, pnt1, pnt2, pnt3));
                            }
                        }
                    }
                    linePoints.push({x: lineXYPnts[pntNum - 1].x, y: lineXYPnts[pntNum - 1].y});
                }

                linesPoints.push(linePoints);
            }

            levelDatas['level'] = level;
            levelDatas['linesPoints'] = linesPoints;
            returnData.push(levelDatas);
        }

        var returnMslData = {};
        // 气压增加高低压处理
        if (isPressure) {
            returnMslData["isoline"] = returnData;
            var high = [];// 存放转换后的高压数据
            var low = [];// 存放转换后的低压数据
            var highArr = data.srcData[0]["high"];
            var lowArr = data.srcData[0]["low"];

            // 高压数据转换
            for (var i = 0, len = highArr.length; i < len; i += 3) {
                var lon = highArr[i];
                var lat = highArr[i + 1];
                var mslVal = highArr[i + 2];
                var pntPxl = OlCommon.getPixelFromLngLat([lon, lat]);
                high.push(pntPxl.x);
                high.push(pntPxl.y);
                high.push(mslVal);
            }

            // 低压数据转换
            for (var i = 0, len = lowArr.length; i < len; i += 3) {
                var lon = lowArr[i];
                var lat = lowArr[i + 1];
                var mslVal = lowArr[i + 2];
                var pntPxl = OlCommon.getPixelFromLngLat([lon, lat]);
                low.push(pntPxl.x);
                low.push(pntPxl.y);
                low.push(mslVal);
            }

            returnMslData["high"] = high;
            returnMslData["low"] = low;
        }

        return isPressure ? returnMslData : returnData;
    }

    var isoline = {
        clear: function () {
            var size = _map.getSize();
            var canvasW = size[0];
            var canvasH = size[1];
            var isolineCtx = _canvas.getContext("2d");
            isolineCtx.clearRect(0, 0, canvasW, canvasH);
        },
        draw: function (data) {
            var size = _map.getSize();
            var getZoom = _map.getView().getCZoom || _map.getView().getZoom;
            var zoom = getZoom();

            // 计算0-360经度的屏幕宽度
            var earthWidth = OlCommon.getWorldPixelWidth();

            // 获取浏览器视窗内最小经度、最大经度
            var extent = OlCommon.getViewportExtent_EPSG4326();
            var leftLon = extent[0];
            var rightLon = extent[2];

            // 计算视窗内有几个世界宽度
            var worldWidthNum = OlCommon.getWorldMaxNum(leftLon, rightLon);

            // 计算第一屏世界需要在0-360基础上的宽度偏移量
            var firstOffsetX = OlCommon.getFirstWordOffsetX(leftLon, earthWidth);

            var productType = data.productType;
            var isPressure = productType == "msl";

            _levelFilter = data.valueFilter;

            var responseData = dealIsolineData(data, isPressure, extent, zoom);

            _canvas.width = size[0];
            _canvas.height = size[1];
            var canvasW = size[0];
            var canvasH = size[1];

            var isolineCtx = _canvas.getContext("2d");
            isolineCtx.clearRect(0, 0, canvasW, canvasH);

            var resData = isPressure ? responseData["isoline"] : responseData;
            var highData = null;
            var lowData = null;
            if (isPressure) {
                highData = responseData["high"];
                lowData = responseData["low"];
            }

            var startT = new Date().getTime();
            for (var i = 0, len1 = resData.length; i < len1; i++) {
                var level = resData[i].level;// 等级
                var linesPoints = resData[i].linesPoints;// 等级对应的所有线条像素点坐标

                for (var j = 0, len2 = linesPoints.length; j < len2; j++) {
                    var linePoints = linesPoints[j];// 线条像素点坐标数据

                    // 循环多屏世界个数，绘制同一条等值线
                    for (var k = 0; k < worldWidthNum; k++) {
                        var offsetX = firstOffsetX + earthWidth * k;
                        drawContour(isolineCtx, productType, zoom, level, linePoints, null, canvasW, canvasH, offsetX);
                        if (isPressure && highData != null && lowData != null) {
                            drawHighLowMark(isolineCtx, highData, lowData, offsetX);
                        }
                    }

                }
            }
            var endT = new Date().getTime();
            console.log('render isoline layer take up time: ' + (endT - startT) + ' ms');
        }
    };

    return isoline;
}

// 方向图
function Arrow(options) {

    var _map = null;
    var _canvas = options.canvas;

    var _imgSize = 32;
    var _imgSpace = 40;

    var arrowImg;// 图片数组
    var imgObj = {};
    var _imgLoaded = false;

    /**
     * 加载方向箭头图标,图片数组、元素量
     */
    function loadImg(cb) {
        if (_imgLoaded) {
            cb();
            return;
        }
        var count = 0;

        // 加载方向箭头图标,图片数组、元素量
        arrowImg = document.createElement("img");
        arrowImg.src = basePath + "resources/img/arrow/arrow.png" + appVersion;
        wait(arrowImg);

        // 加载风向杆图标
        imgObj["2"] = document.createElement("img");
        imgObj["2"].src = basePath + "resources/img/wind/2.png"+ appVersion;
        wait(imgObj['2']);

        for (var i = 3; i < 24; i++) {
            imgObj[i] = document.createElement("img");
            imgObj[i].src = basePath + "resources/img/wind/" + i + ".png"+ appVersion;
            wait(imgObj[i]);
            //镜像版
            imgObj[i+"-m"] = document.createElement("img");
            imgObj[i+"-m"].src = basePath + "resources/img/wind/" + i + "-m.png"+ appVersion;
            wait(imgObj[i+"-m"]);
        }

        function wait(imgNode) {
            count++;
            imgNode.onload = function () {
                count--;
                if (count === 0) {
                    _imgLoaded = true;
                    cb();
                }
            };
        }
    }

    /**
     * 根据uv分量获取速度和角度
     */
    function getWindSpeedAndDirection(u, v) {
        // Map<String, String> data = new HashMap<>();
        var data = {
            speed : "",
            direction : ""
        };
        if (u == 9999.0 && v == 9999.0) {// 9999为缺值
            data.speed = "-1";
            data.direction = "-1";
            return data;
        }
        // 风速
        var speed = 0.0;
        // 风向
        var direction = 0.0;
        // 当水平和垂直风不同时为0时做如下处理
        if (!(u == 0 && v == 0)) {
            speed = Math.sqrt(u * u + v * v);

            if (u >= 0) {
                direction = Math.PI * 3 / 2 - Math.atan(v / u);
            } else {
                direction = Math.PI * 1 / 2 - Math.atan(v / u);
            }

            direction = (direction * 180 / Math.PI);
        }
        //data.speed = speedToScale(speed);
        data.speed = speed;
        data.direction = direction.toString();
        return data;
    }

    /**
     * @Description: 绘制箭头
     * @Author: wuzd
     * @Date: 2018年5月13日 下午8:21:07
     * @param ctx 画笔对象
     * @param x 水平像素
     * @param y 垂直像素
     * @param d 方向
     */
    function drawArrow(ctx, x, y, d, img, size) {
        // 开始绘制
        ctx.save();
        ctx.translate(x, y);// 设置中心点
        ctx.rotate(d * Math.PI / 180);// 旋转角度

        if (img){
            ctx.drawImage(img, (0 - size / 2) , - size, size, size);// 图标、中心点、图标大小
        }
        ctx.restore();// 恢复画布状态
    }

    function clear() {
        if(! _map) return null;
        var size = _map.getSize();
        _canvas.width = size[0];
        _canvas.height = size[1];
        var ctx2d = _canvas.getContext("2d");
        ctx2d.clearRect(0, 0, size[0], size[1]);
        return ctx2d;
    }

    function draw(srcData) {
        loadImg(function () {
            var startT = new Date().getTime();
            drawInternal(srcData);
            var endT = new Date().getTime();
            console.log('render arrow layer take up time: ' + (endT - startT) + ' ms');
        });

        function drawInternal(srcData) {
            var ctx2d = clear();

            var viewportExtent = OlCommon.getViewportExtent_EPSG4326();
            var viewportMinLat = viewportExtent[1];
            var viewportMaxLat = viewportExtent[3];

            // 计算0-360经度的屏幕宽度
            var earthWidth = OlCommon.getWorldPixelWidth();
            var oneDU = earthWidth / 360;

            // 获取浏览器视窗内最小经度、最大经度
            var extent = OlCommon.getViewportExtent_EPSG4326();
            var leftLon = extent[0];
            var rightLon = extent[2];

            // 计算视窗内有几个世界宽度
            var worldWidthNum = OlCommon.getWorldMaxNum(leftLon, rightLon);

            // 计算第一屏世界需要在0-360基础上的宽度偏移量
            var firstOffsetX = OlCommon.getFirstWordOffsetX(leftLon, earthWidth);

            var productType = srcData.productType;
            var isUV = productType == 'wind' || productType == 'oceanCurrent';
            var key = srcData.key;
            var data = srcData.srcData;
            var valueFilter = srcData.valueFilter;
            var imgSpace = srcData['imgSpace'] || _imgSpace;
            var imgSize = srcData['imgSize'] || _imgSize;

            var header = data[0].header;
            // 坐标点计算
            var nx = header.nx;// 格点数
            var ny = header.ny;
            var lo1 = header.lo1;// 起始和结束经纬度
            var la1 = header.la1;
            var lo2 = header.lo2;
            var la2 = header.la2;
            var dx = header.dx;// 格距
            var dy = header.dy;

            var uarray = data[0].data;
            var varray = isUV ? data[1].data : null;

            var diluted = imgSpace / (dx * oneDU);
            diluted = diluted < 1 ? 1 : Math.ceil(diluted);

            var d_dx = diluted;// 格距
            var d_dy = diluted;

            var lo = null;
            var la = null;
            for (var y = 0; y < ny; y = y + d_dy) {
                la = la1 - y;// 纬度
                if(la > viewportMaxLat) {
                    continue;
                }
                if (la < viewportMinLat) {// 纬度超出
                    break;
                }
                //纬度判断南北半球
                var latN = la > 0;

                for (var x = 0; x < nx; x = x + d_dx) {
                    lo = lo1 + x;// 经度
                    if(lo > lo2) {
                        break;
                    }
                    lo = OlCommon.convertLngIn0_360(lo); // 转换为0-360范围经度

                    var ut = 9999;
                    var vt = 9999;

                    if(uarray) {
                        ut = uarray[(1/dy * y)*nx + 1/dx * x];
                        if(ut==9999 || !ut || ut=="null" || ut=="NaN"){//缺测数据
                            continue;
                        }
                    }
                    if(varray) {
                        vt = varray[(1/dy * y)*nx + 1/dx * x];
                        if(vt==9999 || !vt || vt=="null" || vt=="NaN"){//缺测数据
                            continue;
                        }
                    }

                    var dtemp = ut;
                    var img = null;

                    switch (productType) {
                        case "wind":
                            var sd = getWindSpeedAndDirection(ut, vt);// 获取速度和方向
                            dtemp = parseFloat(sd.direction);
                            var imgFoot = OlCommon.getWindScaleBySpeed(sd.speed);//风速转风级
                            if (!valueFilter || (valueFilter && valueFilter(imgFoot))) {
                                img = latN ? imgObj[imgFoot] : imgObj[imgFoot+'-m'];
                            }

                            break;
                        case "oceanCurrent":
                            var sd = getWindSpeedAndDirection(ut, vt);// 获取速度和方向
                            dtemp = parseFloat(sd.direction) + parseFloat(180);
                            if (!valueFilter || (valueFilter && valueFilter(sd.speed))) {
                                img = arrowImg;
                            }
                            break;
                        default:
                            dtemp = ut * 180/Math.PI;
                            dtemp = dtemp < 0 ? dtemp + 360 : dtemp;
                            img = arrowImg;
                            break;
                    }

                    if (!img) {
                        continue;
                    }

                    // 坐标转换
                    var pix = OlCommon.getPixelFromLngLat([lo, la]);
                    var pix_x = pix.x;
                    var pix_y = pix.y;

                    // 循环多屏世界个数
                    for(var k = 0; k < worldWidthNum; k++) {
                        var offsetX = firstOffsetX + earthWidth * k;
                        var pix_x_k = pix_x + offsetX;
                        if(pix_x_k >= 0 && pix_x_k <= _canvas.width)
                            drawArrow(ctx2d, pix_x_k, pix_y, dtemp, img, imgSize);
                    }
                }
            }
        }

    }

    return {
        clear: clear,
        setMap: function(olMap) {
            _map = olMap;
        },
        draw: draw
    };

}

// 色斑图图层
var SpotGridLayer = function () {
    // 色斑图处理
    var SpotHandler = function () {
        var colorScale = null;
        var _limitMinValue = null;

        var handle = function (url, cb) {

            Tools.ajaxGzipFile(url, function (data) {
                if (data) {
                    var builder = createBuilder(data);
                    cb(builder);
                } else {
                    cb(null);
                }
            });
        };

        var createBuilder = function (set) {
            // lo1 = data.lo1, la1 = data.la1, dx = data.dx, dy = data.dy, nx = data.nx, ny = data.ny;
            var scalarData = set.data;
            return {
                header: set.header,
                data: function (i) {
                    return scalarData[i];
                },
                bilinearInterpolate: bilinearInterpolateScalar,
                isScalar: true,
                isValue: function (x) {
                    return (x && x !== 9999) || x === 0;
                }
            };
        };

        /**
         *  [fi,fj] g00         [si, fj] g10
         *                [i,j]
         *  [fi, sj] g01        [si, sj] g11
         * @param lon
         * @param lat
         */
        var interpolatePoint = function (lon, lat, builder) {
            var data = builder.header;
            var lo1 = data.lo1, la1 = data.la1, dx = data.dx, dy = data.dy, nx = data.nx, ny = data.ny;

            var i = floorMod(lon - lo1, 360) / dx;
            var j = (la1 - lat) / dy;
            var fi = Math.floor(i), si = fi + 1;
            var fj = Math.floor(j), sj = fj + 1;

            var g00 = builder.data(fj * nx + fi);
            var g10 = builder.data(fj * nx + si);
            var g01 = builder.data(sj * nx + fi);
            var g11 = builder.data(sj * nx + si);

            var s1 = builder.isValue(g00);
            var s2 = builder.isValue(g10);
            var s3 = builder.isValue(g01);
            var s4 = builder.isValue(g11);

            if (s1 && s2 && s3 && s4) {
                return builder.bilinearInterpolate(i - fi, j - fj, g00, g10, g01, g11);
            }
            return null;
        };

        var bilinearInterpolateScalar = function (x, y, g00, g10, g01, g11) {
            var rx = (1 - x);
            var ry = (1 - y);
            return g00 * rx * ry + g10 * x * ry + g01 * rx * y + g11 * x * y;
        };

        var floorMod = function (a, n) {
            return a - n * Math.floor(a / n);
        };

        var createColorScaleInternal = function (segments) {
            return {
                bounds: [segments[0][0], segments[segments.length - 1][0]],
                gradient: segmentedColorScale(segments)
            };
        };

        function segmentedColorScale(segments) {
            var points = [], interpolators = [], ranges = [];
            for (var i = 0; i < segments.length - 1; i++) {
                points.push(segments[i + 1][0]);
                interpolators.push(colorInterpolator(segments[i][1], segments[i + 1][1]));
                ranges.push([segments[i][0], segments[i + 1][0]]);
            }

            return function (point, alpha) {
                var i;
                for (i = 0; i < points.length - 1; i++) {
                    if (point <= points[i]) {
                        break;
                    }
                }
                var range = ranges[i];
                return interpolators[i](proportion(point, range[0], range[1]), alpha);
            };
        }

        function colorInterpolator(start, end) {
            var r = start[0], g = start[1], b = start[2];
            var dr = end[0] - r, dg = end[1] - g, db = end[2] - b;
            return function (i, a) {
                return [Math.floor(r + i * dr), Math.floor(g + i * dg), Math.floor(b + i * db), a];
            };
        }

        function proportion(x, low, high) {
            return (clamp(x, low, high) - low) / (high - low);
        }

        function clamp(x, low, high) {
            return Math.max(low, Math.min(x, high));
        }

        return {
            handle: handle,
            changeFactor: function (segments, limitMinValue) {
                colorScale = createColorScaleInternal(segments);
                _limitMinValue = limitMinValue;
                return colorScale;
            },
            fillPointColor: function (imgDataArr, canvasWidth, pixelX, pixelY, lnglat, builder, valueFilter) {
                var value = interpolatePoint(lnglat[0], lnglat[1], builder);
                if (value) {
                    if (_limitMinValue && value < _limitMinValue) {
                        return;
                    }
                    if (valueFilter && !valueFilter(value)) {
                        return;
                    }
                    var rgba = colorScale.gradient(value, 255);
                    if (rgba) {
                        var i;
                        for (var x = pixelX; x <= pixelX + 1; x++) {
                            for (var y = pixelY; y <= pixelY + 1; y++) {
                                i = (y * canvasWidth + x) * 4;
                                imgDataArr[i] = rgba[0];
                                imgDataArr[i + 1] = rgba[1];
                                imgDataArr[i + 2] = rgba[2];
                                imgDataArr[i + 3] = rgba[3];
                            }
                        }
                    }
                }
            }
        };
    };

    return L.GridLayer.extend({
        initialize: function (options) {
            L.Util.setOptions(this, options);
            this.options.defers = [];
            this.options.clayerType = 'spotGridLayer';
            this.options.cache = new lruCache(300);

            this.on('add', function (e) {
                var _layer = this;
                this._map.on('moveend', function (e1) {
                    _layer.resolveDefers();
                })
            });
            this.on('load', function (e) {
                this.resolveDefers();
            });
        },
        createTile: function (coords, done) {
            var _this = this;

            var tile = L.DomUtil.create('canvas', 'leaflet-tile');
            var size = this.getTileSize();
            tile.width = size.x;
            tile.height = size.y;
            var url = _this.options.url;
            var visible = _this.options._visible;
            if (!visible || !_this.options.spotHandler || !url) {
                return tile;
            }

            var defer = $.Deferred();
            defer.done(function () {
                var g = _this._tileCoordsToNwSe(coords);

                var dataZooms = _this.options.dataZooms;
                var x2 = coords.x, y2 = coords.y, z2 = coords.z;
                // fix tileCoords position to data zoom tileCoords
                for (var i = dataZooms.length - 1; i >= 0; i--) {
                    if (z2 >= dataZooms[i]) {
                        var zz = Math.pow(2, z2 - dataZooms[i]);
                        x2 = Math.floor(x2 / zz);
                        y2 = Math.floor(y2 / zz);
                        z2 = dataZooms[i];
                        break;
                    }
                }

                var valueFilter = _this.options.valueFilter;

                var tileUrl = url.replace('{z}', z2)
                    .replace('{x}', x2)
                    .replace('{y}', y2);
                var pixel = _this._map.latLngToLayerPoint(g[0]);
                // async operation
                var spotHandler = _this.options.spotHandler;
                var tileKey = x2 + ':' + y2 + ':' + z2;
                var tileData = _this.options.cache.get(tileKey);

                if (tileData) {
                    renderTile(tileData);
                } else {
                    spotHandler.handle(tileUrl, renderTile);
                }

                function renderTile(builder) {
                    _this.options.cache.put(tileKey, builder);
                    var ctx = tile.getContext("2d");
                    if (builder) {
                        var tileSize = 256;
                        var imgData = ctx.getImageData(0, 0, tileSize, tileSize);
                        var rgbaArr = imgData.data;

                        var x, y;
                        for (var i = 0; i < tileSize; i += 2) {
                            x = pixel.x + i;
                            for (var j = 0; j < tileSize; j += 2) {
                                y = pixel.y + j;
                                var latLng = _this._map.layerPointToLatLng({x: x, y: y});
                                spotHandler.fillPointColor(rgbaArr, tileSize, i, j, [latLng.lng, latLng.lat], builder, valueFilter);
                            }
                        }
                        ctx.putImageData(imgData, 0, 0);
                    }

                    if (_this.options && _this.options.debuged) {
                        ctx.font = "20px serif";
                        var fi = 256 / 8;
                        var text = 'z:' + coords.z + ', x:' + coords.x + ',y:' + coords.y;
                        ctx.fillText(text, 0, fi);

                        ctx.fillText(('redirect tile position:'), 0, fi * 3);
                        ctx.fillText(('z2:' + z2 + ', x2:' + x2 + ',y2:' + y2), 0, fi * 4);

                        ctx.strokeStyle = 'red';
                        ctx.strokeRect(0, 0, 256, 256);
                    }

                    done(null, tile);
                }
            });
            _this.options.defers.push(defer);
            return tile;
        },
        // dataZooms must be order by small zoom to big zoom
        updateUrl: function (url, options) {
            var this_ = this;
            if (url && url !== this_.options.url) {
                this.options.cache.removeAll();
                this_.options.url = url;
            }
            this_.options._visible = !!url;
            if (!this_.options.spotHandler) {
                this_.options.spotHandler = new SpotHandler();
            }
            if (options) {
                if (options['dataZooms']) {
                    this_.options.dataZooms = options['dataZooms'];
                }
                if (options['colorSegments']) {
                    this_.options.colorScale = this_.options.spotHandler.changeFactor(options['colorSegments'], options['limitMinValue']);
                }
                var valueFilter = options['valueFilter'];
                if (valueFilter && typeof valueFilter === 'function') {
                    this_.options.valueFilter = valueFilter;
                } else {
                    this_.options.valueFilter = null;
                }
            }
            console.log('update render the spot layer');
            this_.redraw();
            this_.resolveDefers();
        },
        resolveDefers: function () {
            var defers = this.options.defers;
            if (defers && defers.length) {
                for (; defers.length;) {
                    defers.pop().resolve();
                }
            }
        },
        getColorScale: function () {
            return this.options.colorScale;
        }
    });
}();